/**
 * Actively Replicated Crash Free Single Software Failure (non Byzantine)
 * Distributed Retail Store 
 * 
 * November 27 2012
 */

package comp6231.project.replica.dimitri;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;

import comp6231.project.replica.Replica;
import comp6231.project.common.Constants;
import comp6231.project.common.ReplicaState;

/**
 * @author dimitri.tiago
 * This class realizes a Retail Store Server  
 */
public class RetailStoreServer extends Replica
{   
	private String retailStoreLocation;
	private String retailStoreReplicaID;

	private int UDPStockServerPort;

	private RetailStoreStock storeStock;
	private CustomerDatabase customerDb;
	
	private String [] retailStores = {
	        "montreal-retail-store/3301",
	        "toronto-retail-store/3302",
	        "vancouver-retail-store/3303"};
	
	/**
	 * Default constructor performs superclass initialization
	 * and initializes store stock.
	 * @throws RemoteException
	 */
	public RetailStoreServer(String retailStoreLocation)
	{
		// set retail store server properties
		if (retailStoreLocation.equalsIgnoreCase(Constants.REPLICA_ID_M1))
		{
			this.retailStoreLocation = "montreal-retail-store";
			this.retailStoreReplicaID = Constants.REPLICA_ID_M1;
			this.UDPStockServerPort = 3301;
			this.replicaId = Constants.REPLICA_ID_M1;
		}
		else if (retailStoreLocation.equalsIgnoreCase(Constants.REPLICA_ID_T1))
		{
			this.retailStoreLocation = "toronto-retail-store";
			this.retailStoreReplicaID = Constants.REPLICA_ID_T1;
			this.UDPStockServerPort = 3302;
			this.replicaId = Constants.REPLICA_ID_T1;
		}
		else
		{
			this.retailStoreLocation = "vancouver-retail-store";
			this.retailStoreReplicaID = Constants.REPLICA_ID_V1;
			this.UDPStockServerPort = 3303;
			this.replicaId = Constants.REPLICA_ID_V1;
		}
			
		storeStock = new RetailStoreStock(this.retailStoreLocation);

		// start udp retail stock server
		UDPRetailStockServer updRetailStockServer = new UDPRetailStockServer(UDPStockServerPort, storeStock);
		Thread stockServerThread = new Thread(updRetailStockServer);
		stockServerThread.start();

		// initialize customer database file
		customerDb = new CustomerDatabase(retailStoreLocation);
	}

    public int buyItems(String customerID, int itemID, int numberOfItem)
    {
        int transactionResult = 0; // default transaction response

        // remove requested number of item from stock
        int itemsFromStock = storeStock.remItemsFromStock(itemID, numberOfItem);
        if (itemsFromStock == numberOfItem) {
            // we have enough items to satisfy request
            transactionResult = itemsFromStock;

            // update customer record in customer database file
            customerDb.insertRecord(customerID, itemID, transactionResult);
        } else {
            // we do not have enough items to satisfy request attempt to buy from another store
            try {
                // attempt to buy remaining items from other stores
                String currStoreRegistryLocation = String.format("%s/%s", retailStoreLocation, UDPStockServerPort);
                int remainingItems = numberOfItem - itemsFromStock; // remaining items needed to fulfill request
                int itemsBoughtAtNextStore = 0;
                for (String store : retailStores) // attempt to buy remaining items from other stores 
                {
                    if (!(store.equalsIgnoreCase(currStoreRegistryLocation))) // exclude current store.
                    {
                        // attempt to buy remaining items from next store
                        // using udp retail stock server.
                        try {
                            // start client thread to request stock
                            int UDPClientPort = Integer.parseInt(store.split("/")[1].trim());
                            UPDRetailStockClient UDPClient = new UPDRetailStockClient(UDPClientPort, itemID, remainingItems); // create buy items client

                            Thread udpClientThread = new Thread(UDPClient);
                            udpClientThread.start();
                            udpClientThread.join();

                            itemsBoughtAtNextStore += UDPClient.getNumberOfItems(); // get items requested from next store.
                            remainingItems -= UDPClient.getNumberOfItems(); // update remaining items
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }

                        if (remainingItems == 0) // we can fulfill order
                        {
                            // order is fulfilled
                            transactionResult = itemsFromStock + itemsBoughtAtNextStore;

                            // record purchase in customer database
                            customerDb.insertRecord(customerID, itemID, transactionResult);

                            break; // exit for loop
                        }
                    }
                }

                // we were not able to meet request
                if (remainingItems != 0) {
                    // add items from next stores to local stock
                    int numItemsToReturn = itemsFromStock + itemsBoughtAtNextStore;
                    storeStock.addItemsToStock(itemID, numItemsToReturn);
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }

        // return number of items bought (0 if we are not able to fulfill request)
        return transactionResult;
    }

    public int returnItems(String customerID, int itemID, int numberOfItem) 
    {
        int itemsReturned = 0;

        // determine how many items customer has bought in database
        String[] selectedRecord = customerDb.selectRecord(customerID, itemID);
        if (selectedRecord == null) {
            // customer never bought item at store 
            // do not allow return (itemsReturned = 0)
        } else {
            // determine if items to return are <= than number of items bought
            // stored in customer record in database.
            int recNumberOfItems = Integer.parseInt(selectedRecord[2]);
            if (recNumberOfItems >= numberOfItem) {
                // allow return of items
                itemsReturned = storeStock.addItemsToStock(itemID, numberOfItem);

                // update customer database record with returned items
                customerDb.updateRecord(customerID, itemID, numberOfItem);
            } else {
                // customer returning more items than bought 
                // do not allow return (itemsReturned = 0)
            }
        }

        return itemsReturned;
    }

    public int checkStock(int itemID) 
    {
    	int itemsInStock = 0;
    	
    	if (itemID == 2000)
    	{
    		itemsInStock = 10;
    	}
    	else
    	{
    		//TODO: commented out to insert error to test replacement
	        //int itemsInStock = 0;
	        ArrayList<String> otherRetailStores = new ArrayList<String>();;
	
	        // obtain store registry names for retail stores
	        try 
	        {
	            String currStoreRegistryLocation = String.format("%s/%d", retailStoreLocation, UDPStockServerPort);
	            for (String store: retailStores) 
	            {   
	                if (!(store.equalsIgnoreCase(currStoreRegistryLocation))) // exclude current store.
	                { 
	                    otherRetailStores.add(store); // build  list of other retail stores
	                } 
	            }
	        } catch (Exception e) {
	            e.printStackTrace();
	        }
	
	        // instantiate two threads (one for each other retail store)
	        // and check their stocks
	        String UDPClient1Port = ((otherRetailStores.get(0).split("/"))[1].trim()); // obtain udp server 1 port	
	        String UDPClient2Port = ((otherRetailStores.get(1).split("/"))[1].trim()); // obtain udp server 2 port
	
	        UPDRetailStockClient UDPClient1 = new UPDRetailStockClient(itemID, Integer.parseInt(UDPClient1Port));
	        UPDRetailStockClient UDPClient2 = new UPDRetailStockClient(itemID, Integer.parseInt(UDPClient2Port));
	
	        Thread udpClientThread1 = new Thread(UDPClient1);
	        Thread udpClientThread2 = new Thread(UDPClient2);
	
	        udpClientThread1.start();
	        udpClientThread2.start();
	
	        try {
	            udpClientThread1.join();
	            udpClientThread2.join();
	        } catch (InterruptedException e) {
	            e.printStackTrace();
	        }
	
	        itemsInStock = storeStock.checkItemsStock(itemID) + UDPClient1.getNumberOfItems() + UDPClient2.getNumberOfItems();
    	}
    	
        return itemsInStock;
    }

    public int exchangeItems(String customerID, int boughtItemID, int boughtNumber, int desiredItemID, int desiredNumber) 
    {
        int itemsExchanged = 0;

        // determine how many items customer has bought in database
        String[] selectedRecord = customerDb.selectRecord(customerID, boughtItemID);
        if (selectedRecord == null) {
            // customer never bought item at store 
            // do not allow exchange (itemsExchanged = 0)
        } else {
            // determine if items to exchange are <= than number of items bought
            // stored in customer record in database.
            int recNumberOfItems = Integer.parseInt(selectedRecord[2]);
            if (recNumberOfItems >= boughtNumber) {
                // allow exchange of items

                // attempt to buy number of items required for exchange
                itemsExchanged = buyItems(customerID, desiredItemID, desiredNumber);
                if (itemsExchanged == 0) {
                    // there are not enough desired items available 
                    // at retail store to exchange.

                    return itemsExchanged;
                } else {
                    // exchange accepted, return previously bought items
                    storeStock.addItemsToStock(boughtItemID, boughtNumber);

                    // update customer database with returned number of items
                    customerDb.updateRecord(customerID, boughtItemID, boughtNumber);
                }
            } else {
                // customer exchanging more items than bought 
                // do not allow exchange (itemsExchanged = 0)
            }
        }

        return itemsExchanged;
    }

	@Override
	public int buyOp(String customerID, String itemID, int numberOfItem) 
	{	
		return buyItems(customerID, Integer.parseInt(itemID), 
				numberOfItem);
	}

	@Override
	public int returnOp(String customerID, String itemID, int numberOfItem) 
	{	
		return returnItems(customerID, Integer.parseInt(itemID), 
				numberOfItem);
	}

	@Override
	public int checkStockOp(String itemID) 
	{
		return checkStock(Integer.parseInt(itemID));
	}

	@Override
	public int exchangeOp(
			String customerID, 
			String boughtItemID,
			int boughtNumber,
			String desiredItemID, 
			int desiredNumber) 
	{
		return exchangeItems(customerID, Integer.parseInt(boughtItemID), 
				boughtNumber, Integer.parseInt(desiredItemID), desiredNumber);
	}

	@Override
	public ReplicaState getReplicaState() 
	{
		ReplicaState replicaState = new ReplicaState();
		
		// extract items state
		HashMap<Integer, Integer> itemsStock = storeStock.exportItemsStock(); // get stock (itemId, numOfItems)
		Map<Integer, Integer> map = itemsStock;
		Iterator<Map.Entry<Integer, Integer>> entries = map.entrySet().iterator();
		while (entries.hasNext()) 
		{
		    Map.Entry<Integer, Integer> entry = entries.next();
		    replicaState.addItem(String.format("%04d", entry.getKey()), entry.getValue()); // add items state to replica state
		}
		
		// extract customers state
		ArrayList<RandomAccessCustomerRecord> customerRecords = customerDb.exportRecords();
		for (RandomAccessCustomerRecord r : customerRecords)
		{
			replicaState.addCustomerBoughtItem(r.getCustomerID(), 
					String.format("%04d", r.getItemID()), r.getNumberOfItems());			
		}
		
		return replicaState;
	}
	
	@Override
	public void setReplicaState(ReplicaState state) 
	{
		Map<String,Integer> items = state.getItems();
		storeStock.importItemsStock(items);
	
		// obtain and import customer records
		Set<String> customers = state.getCustomers();
		ArrayList<RandomAccessCustomerRecord> recordsToImport = new ArrayList<RandomAccessCustomerRecord>();
		for (String customerID : customers)
		{
			// iterate for each customer and obtain customer records
			Map<String, Integer> itemsBought = state.getCustomerBoughtItems(customerID);
			Iterator<Map.Entry<String, Integer>> entries = itemsBought.entrySet().iterator();
			while (entries.hasNext()) 
			{
			    Map.Entry<String, Integer> entry = entries.next();
			    
			    // create customer record and add to list of 
			    // customers to import
			    RandomAccessCustomerRecord customerRecord = new RandomAccessCustomerRecord(customerID, Integer.parseInt(entry.getKey()), entry.getValue()); 
			    recordsToImport.add(customerRecord);
			}
		}
		
		customerDb.importRecords(recordsToImport);
	}
}